package jmine.tec.aom.domain;

import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;

import jmine.tec.aom.exception.FillException;
import jmine.tec.persist.annotation.Alias;
import jmine.tec.persist.annotation.Comment;
import jmine.tec.persist.annotation.Constraint;
import jmine.tec.persist.annotation.Index;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Filters;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.validator.NotNull;

import bancosys.tec.component.Documentation;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.bussobj.AuthPersistableBusinessObject;

/**
 * Abstract definition of the value of some {@link Property}.
 * 
 * @param <T> the actual type stored by this {@link AbstractPropertyValue}
 * @author jrenaut
 */
@Entity
@Table(name = "CARACTERISTICA_VALOR")
@Alias("CARCVL")
@Documentation("ARMAZENA OS VALORES DE CARACTERISTICAS.")
@SequenceGenerator(name = "SEQ_CARCVL", sequenceName = "SEQ_CARCVL")
@DiscriminatorColumn(name = "TIPO", discriminatorType = DiscriminatorType.INTEGER)
@Constraint(suffix = "0")
@Comment(table = "CARACTERISTICA_VALOR", column = "TIPO", value = "IDENTIFICA O TIPO DO VALOR DE CARACTERISTICA.")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Filters({ @Filter(name = "authFilter", condition = "auth = :auth") })
@DisplayName(value = "Caracteristica Valor", showAuth = false)
public abstract class AbstractPropertyValue<T> extends AuthPersistableBusinessObject {

    private Long id;

    private Property property;

    private boolean locked;

    private T value;

    /**
     */
    public AbstractPropertyValue() {
    }

    /**
     * @return the actual value stored
     */
    @Transient
    public T getValue() {
        return this.value;
    }

    /**
     * @param value the value to store
     */
    public void setValue(T value) {
        this.value = value;
    }

    /**
     * Stores the given value performing the required type conversion.
     * 
     * @param stringValue the value as a {@link String}
     * @throws FillException if the value cannot be converted
     */
    public void setValueAsString(String stringValue) throws FillException {
        if (StringUtils.isBlank(stringValue)) {
            this.value = null;
        } else {
            this.doSetValue(stringValue);
        }
    }

    /**
     * Hook for implementations to perform type conversion. Implementations can assume the string value is neither null nor empty.
     * 
     * @param stringValue the value as a {@link String}
     * @throws FillException if the value cannot be converted
     */
    public abstract void doSetValue(String stringValue) throws FillException;

    /**
     * @return the stored value as a {@link String}
     */
    @Transient
    @DisplayName(value = "Valor", order = "12")
    public final String getValueAsString() {
        return this.getValue() == null ? null : this.doGetValueAsString();
    }

    /**
     * Default implementation delegates to toString() on non null values.
     * 
     * @return the stored value as a {@link String}
     */
    protected String doGetValueAsString() {
        return this.getValue().toString();
    }

    /**
     * @return true if this property value has been defined by use of the SyntheticProperty annotation
     */
    @Transient
    public boolean isSynthetic() {
        return false;
    }

    /**
     * @return the {@link Property}
     */
    @NotNull
    @ManyToOne
    @Index(suffix = "JT_0")
    @DisplayName("Característica")
    @JoinColumn(name = "COD_CARACTERISTICA", nullable = false, updatable = false)
    @Documentation("CODIGO DA CARACTERISTICA")
    @ForeignKey(name = "FK_CARACT_CARCVL")
    public Property getProperty() {
        return this.property;
    }

    /**
     * @return the id
     */
    @Id
    @Column(name = "COD_CARAC_VL")
    @Documentation("CODIGO DA CARACTERISTICA DE VALORES")
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CARCVL")
    public Long getId() {
        return this.id;
    }

    /**
     * @return the name of the {@link Property}
     */
    @Transient
    @DisplayName(value = "Nome", order = "10")
    public String getPropertyName() {
        return this.getProperty().getName();
    }

    /**
     * @return the description of the {@link Property}
     */
    @Transient
    @DisplayName(value = "Descrição", order = "11")
    public String getPropertyDescription() {
        return this.getProperty().getDescription();
    }

    /**
     * @param property a {@link Property}
     */
    public void setProperty(final Property property) {
        this.property = property;
    }

    /**
     * @param id the id to set
     */
    public void setId(final Long id) {
        this.id = id;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!this.getClass().equals(obj.getClass())) {
            return false;
        }
        AbstractPropertyValue<?> o = (AbstractPropertyValue<?>) obj;
        return new EqualsBuilder().append(this.property.getName(), o.getProperty().getName()).append(this.getValue(), o.getValue())
                .isEquals();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(this.property.getName()).append(this.getValue()).toHashCode();
    }

    /**
     * @return a copy of this {@link AbstractPropertyValue}
     */
    public final AbstractPropertyValue<T> copy() {
        AbstractPropertyValue<T> cv = this.createBean();
        cv.setProperty(this.property);
        cv.setLocked(this.locked);
        cv.setValue(this.copyValue());
        return cv;
    }

    /**
     * Default implementation returns the stored object untouched.
     * 
     * @return the value to be used on a copied {@link AbstractPropertyValue}
     */
    protected T copyValue() {
        return this.getValue();
    }

    /**
     * @return a new instance of this type of {@link AbstractPropertyValue}
     */
    @SuppressWarnings("unchecked")
    protected AbstractPropertyValue<T> createBean() {
        return this.getController().getDAOFactory().getGenericDAO(this.getClass()).createBean();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return String.format("CaracteristicaValor:%s,%s,%s", this.getId(), this.getProperty(), this.getValueAsString());
    }

    /**
     * Performs the merge of this {@link AbstractPropertyValue} and the given one.
     * 
     * @param other {@link AbstractPropertyValue}
     * @return {@link AbstractPropertyValue}
     */
    public final AbstractPropertyValue<T> merge(AbstractPropertyValue<T> other) {
        if (this.isLocked()) {
            return this;
        }
        return this.doMerge(other);
    }

    /**
     * May be overriden.
     * <p>
     * Default behavior returns the other {@link AbstractPropertyValue} if it is not null. This one otherwise.
     * 
     * @param other another {@link AbstractPropertyValue}
     * @return the merged {@link AbstractPropertyValue}
     */
    protected AbstractPropertyValue<T> doMerge(AbstractPropertyValue<T> other) {
        return other != null ? other : this;
    }

    /**
     * @return the locked
     */
    @Constraint(suffix = "JT_1")
    @Column(name = "LOCKED")
    @Documentation("INDICA QUE OS VALORES DESSE REGISTRO  PODEM SER SOBRESCRITOS POR VALORES DE HIERARQUIA INFERIOR (0) OU NAO (1)")
    @NotNull
    public boolean isLocked() {
        return this.locked;
    }

    /**
     * @param locked the locked to set
     */
    public void setLocked(boolean locked) {
        this.locked = locked;
    }
}
